SpringCloud微服务实战 您所在的位置:网站首页 springcloud 鉴权 SpringCloud微服务实战

SpringCloud微服务实战

2023-10-16 13:13| 来源: 网络整理| 查看: 265

 OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间、限定范围访问指定资源。

  OAuth2中使用token验证用户登录合法性,但token最大的问题是不携带用户信息,资源服务器无法在本地进行验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低,且认证服务器会变成一个中心节点,这在分布式架构下很影响性能。如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。

  SpringCloud认证授权解决思路:认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

微服务鉴权功能划分:

gitegg-oauth:Oauth2用户认证和单点登录

gitegg-gateway:请求转发和统一鉴权

gitegg-system: 读取系统配置的RBAC权限配置并存放到缓存

一、鉴权配置

1、GitEgg-Platform工程下新建gitegg-platform-oauth2工程,用于统一管理OAuth2版本,及统一配置

GitEgg-Platform com.gitegg.platform 1.0-SNAPSHOT 4.0.0 gitegg-platform-oauth2 ${project.artifactId} jar org.springframework.boot spring-boot-configuration-processor org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.security spring-security-oauth2-jose org.springframework.security spring-security-oauth2-resource-server com.gitegg.platform gitegg-platform-swagger true

2、在gitegg-oauth工程中引入需要的库

GitEgg-Cloud com.gitegg.cloud 1.0-SNAPSHOT 4.0.0 gitegg-oauth ${project.artifactId} jar com.gitegg.platform gitegg-platform-boot ${gitegg.project.version} com.gitegg.platform gitegg-platform-cloud ${gitegg.project.version} com.gitegg.platform gitegg-platform-oauth2 ${gitegg.project.version} com.gitegg.platform gitegg-platform-db com.gitegg.platform gitegg-platform-mybatis com.gitegg.platform gitegg-platform-captcha com.gitegg.cloud gitegg-service-system-api ${gitegg.project.version} org.apache.tomcat.embed tomcat-embed-core org.springframework.boot spring-boot-starter-data-redis

3、JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。首先我们使用keytool生成RSA证书gitegg.jks,复制到gitegg-oauth工程的resource目录下,CMD命令行进入到JDK安装目录的bin目录下, 使用keytool命令生成gitegg.jks证书

keytool -genkey -alias gitegg -keyalg RSA -keystore gitegg.jks

4、新建GitEggUserDetailsServiceImpl.java实现SpringSecurity获取用户信息接口,用于SpringSecurity鉴权时获取用户信息

package com.gitegg.oauth.service; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import com.gitegg.oauth.enums.AuthEnum; import com.gitegg.platform.base.constant.AuthConstant; import com.gitegg.platform.base.domain.GitEggUser; import com.gitegg.platform.base.enums.ResultCodeEnum; import com.gitegg.platform.base.result.Result; import com.gitegg.service.system.api.feign.IUserFeign; import cn.hutool.core.bean.BeanUtil; import lombok.RequiredArgsConstructor; /** * 实现SpringSecurity获取用户信息接口 * * @author gitegg */ @Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class GitEggUserDetailsServiceImpl implements UserDetailsService { private final IUserFeign userFeign; private final HttpServletRequest request; @Override public GitEggUserDetails loadUserByUsername(String username) { // 获取登录类型,密码,二维码,验证码 String authLoginType = request.getParameter(AuthConstant.AUTH_TYPE); // 获取客户端id String clientId = request.getParameter(AuthConstant.AUTH_CLIENT_ID); // 远程调用返回数据 Result result; // 通过手机号码登录 if (!StringUtils.isEmpty(authLoginType) && AuthEnum.PHONE.code.equals(authLoginType)) { String phone = request.getParameter(AuthConstant.PHONE_NUMBER); result = userFeign.queryUserByPhone(phone); } // 通过账号密码登录 else if(!StringUtils.isEmpty(authLoginType) && AuthEnum.QR.code.equals(authLoginType)) { result = userFeign.queryUserByAccount(username); } else { result = userFeign.queryUserByAccount(username); } // 判断返回信息 if (null != result && result.isSuccess()) { GitEggUser gitEggUser = new GitEggUser(); BeanUtil.copyProperties(result.getData(), gitEggUser, false); if (gitEggUser == null || gitEggUser.getId() == null) { throw new UsernameNotFoundException(ResultCodeEnum.INVALID_USERNAME.msg); } if (CollectionUtils.isEmpty(gitEggUser.getRoleIdList())) { throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_ROLE.msg); } return new GitEggUserDetails(gitEggUser.getId(), gitEggUser.getTenantId(), gitEggUser.getOauthId(), gitEggUser.getNickname(), gitEggUser.getRealName(), gitEggUser.getOrganizationId(), gitEggUser.getOrganizationName(), gitEggUser.getOrganizationIds(), gitEggUser.getOrganizationNames(), gitEggUser.getRoleId(), gitEggUser.getRoleIds(), gitEggUser.getRoleName(), gitEggUser.getRoleNames(), gitEggUser.getRoleIdList(), gitEggUser.getRoleKeyList(), gitEggUser.getResourceKeyList(), gitEggUser.getDataPermission(), gitEggUser.getAvatar(), gitEggUser.getAccount(), gitEggUser.getPassword(), true, true, true, true, AuthorityUtils.createAuthorityList(gitEggUser.getRoleIdList().toArray(new String[gitEggUser.getRoleIdList().size()]))); } else { throw new UsernameNotFoundException(result.getMsg()); } } }

5、新建AuthorizationServerConfig.java用于认证服务相关配置,正式环境请一定记得修改gitegg.jks配置的密码,这里默认为123456。TokenEnhancer 为登录用户的扩展信息,可以自己定义。

package com.gitegg.oauth.config; import java.security.KeyPair; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.TokenGranter; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import com.anji.captcha.service.CaptchaService; import com.gitegg.oauth.granter.GitEggTokenGranter; import com.gitegg.oauth.service.GitEggClientDetailsServiceImpl; import com.gitegg.oauth.service.GitEggUserDetails; import com.gitegg.platform.base.constant.AuthConstant; import com.gitegg.platform.base.constant.TokenConstant; import com.gitegg.service.system.api.feign.IUserFeign; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; /** * 认证服务配置 */ @Configuration @EnableAuthorizationServer @RequiredArgsConstructor(onConstructor_ = @Autowired) public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private final DataSource dataSource; private final AuthenticationManager authenticationManager; private final UserDetailsService userDetailsService; private final IUserFeign userFeign; private final RedisTemplate redisTemplate; private final CaptchaService captchaService; @Value("${captcha.type}") private String captchaType; /** * 客户端信息配置 */ @Override @SneakyThrows public void configure(ClientDetailsServiceConfigurer clients) { GitEggClientDetailsServiceImpl jdbcClientDetailsService = new GitEggClientDetailsServiceImpl(dataSource); jdbcClientDetailsService.setFindClientDetailsSql(AuthConstant.FIND_CLIENT_DETAILS_SQL); jdbcClientDetailsService.setSelectClientDetailsSql(AuthConstant.SELECT_CLIENT_DETAILS_SQL); clients.withClientDetails(jdbcClientDetailsService); } /** * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services) */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); List tokenEnhancers = new ArrayList(); tokenEnhancers.add(tokenEnhancer()); tokenEnhancers.add(jwtAccessTokenConverter()); tokenEnhancerChain.setTokenEnhancers(tokenEnhancers); // 获取自定义tokenGranter TokenGranter tokenGranter = GitEggTokenGranter.getTokenGranter(authenticationManager, endpoints, redisTemplate, userFeign, captchaService, captchaType); endpoints.authenticationManager(authenticationManager) .accessTokenConverter(jwtAccessTokenConverter()) .tokenEnhancer(tokenEnhancerChain) .userDetailsService(userDetailsService) .tokenGranter(tokenGranter) /** * * refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true * 1.重复使用:access_token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准 * 2.非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新而无需失效再次登录 */ .reuseRefreshTokens(false); } /** * 允许表单认证 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) { security.allowFormAuthenticationForClients() .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } /** * 使用非对称加密算法对token签名 */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setKeyPair(keyPair()); return converter; } /** * 从classpath下的密钥库中获取密钥对(公钥+私钥) */ @Bean public KeyPair keyPair() { KeyStoreKeyFactory factory = new KeyStoreKeyFactory( new ClassPathResource("gitegg.jks"), "123456".toCharArray()); KeyPair keyPair = factory.getKeyPair( "gitegg", "123456".toCharArray()); return keyPair; } /** * JWT内容增强 */ @Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { Map map = new HashMap(2); GitEggUserDetails user = (GitEggUserDetails) authentication.getUserAuthentication().getPrincipal(); map.put(TokenConstant.TENANT_ID, user.getTenantId()); map.put(TokenConstant.OAUTH_ID, user.getOauthId()); map.put(TokenConstant.USER_ID, user.getId()); map.put(TokenConstant.ORGANIZATION_ID, user.getOrganizationId()); map.put(TokenConstant.ORGANIZATION_NAME, user.getOrganizationName()); map.put(TokenConstant.ORGANIZATION_IDS, user.getOrganizationIds()); map.put(TokenConstant.ORGANIZATION_NAMES, user.getOrganizationNames()); map.put(TokenConstant.ROLE_ID, user.getRoleId()); map.put(TokenConstant.ROLE_NAME, user.getRoleName()); map.put(TokenConstant.ROLE_IDS, user.getRoleIds()); map.put(TokenConstant.ROLE_NAMES, user.getRoleNames()); map.put(TokenConstant.ACCOUNT, user.getAccount()); map.put(TokenConstant.REAL_NAME, user.getRealName()); map.put(TokenConstant.NICK_NAME, user.getNickname()); map.put(TokenConstant.ROLE_ID_LIST, user.getRoleIdList()); map.put(TokenConstant.ROLE_KEY_LIST, user.getRoleKeyList()); //不把权限菜单放到jwt里面,当菜单太多时,会导致jwt长度不可控 // map.put(TokenConstant.RESOURCE_KEY_LIST, user.getResourceKeyList()); map.put(TokenConstant.DATA_PERMISSION, user.getDataPermission()); map.put(TokenConstant.AVATAR, user.getAvatar()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map); return accessToken; }; } }

6、Gateway在认证授权时需要RSA的公钥来验证签名是否合法,所以这里新建GitEggOAuthController的getKey接口用于Gateway获取RSA公钥

@GetMapping("/public_key") public Map getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); }

7、新建ResourceServerConfig.java资源服务器配置,放开public_key的读取权限

@Override @SneakyThrows public void configure(HttpSecurity http) { http.headers().frameOptions().disable(); http.formLogin() .and() .authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .and() .authorizeRequests() .antMatchers( "/oauth/public_key").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }

8、在gitegg-service-system新建InitResourceRolesCacheRunner.java实现CommandLineRunner接口,用于系统启动时加载RBAC权限配置信息到缓存

package com.gitegg.service.system.component; import java.util.*; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import com.gitegg.platform.base.constant.AuthConstant; import com.gitegg.service.system.entity.Resource; import com.gitegg.service.system.service.IResourceService; import cn.hutool.core.collection.CollectionUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * 容器启动完成加载资源权限数据到缓存 */ @Slf4j @RequiredArgsConstructor(onConstructor_ = @Autowired) @Component public class InitResourceRolesCacheRunner implements CommandLineRunner { private final RedisTemplate redisTemplate; private final IResourceService resourceService; /** * 是否开启租户模式 */ @Value(("${tenant.enable}")) private Boolean enable; @Override public void run(String... args) { log.info("InitResourceRolesCacheRunner running"); // 查询系统角色和权限的关系 List resourceList = resourceService.queryResourceRoleIds(); // 判断是否开启了租户模式,如果开启了,那么角色权限需要按租户进行分类存储 if (enable) { Map resourceListMap = resourceList.stream().collect(Collectors.groupingBy(Resource::getTenantId)); resourceListMap.forEach((key, value) -> { String redisKey = AuthConstant.TENANT_RESOURCE_ROLES_KEY + key; redisTemplate.delete(redisKey); addRoleResource(redisKey, value); System.out.println(redisTemplate.opsForHash().entries(redisKey).size()); }); } else { redisTemplate.delete(AuthConstant.RESOURCE_ROLES_KEY); addRoleResource(AuthConstant.RESOURCE_ROLES_KEY, resourceList); } } private void addRoleResource(String key, List resourceList) { Map resourceRolesMap = new TreeMap(); Optional.ofNullable(resourceList).orElse(new ArrayList()).forEach(resource -> { // roleId -> ROLE_{roleId} List roles = Optional.ofNullable(resource.getRoleIds()).orElse(new ArrayList()).stream() .map(roleId -> AuthConstant.AUTHORITY_PREFIX + roleId).collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(roles)) { resourceRolesMap.put(resource.getResourceUrl(), roles); } }); redisTemplate.opsForHash().putAll(key, resourceRolesMap); } }

9、新建网关服务gitegg-gateway,作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行转发、统一校验认证和鉴权操作,引入相关依赖

GitEgg-Cloud com.gitegg.cloud 1.0-SNAPSHOT 4.0.0 gitegg-gateway com.gitegg.platform gitegg-platform-base ${gitegg.project.version} com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.cloud spring-cloud-starter-openfeign com.gitegg.platform gitegg-platform-oauth2 ${gitegg.project.version} com.gitegg.platform gitegg-platform-cache ${gitegg.project.version} org.springframework.cloud spring-cloud-starter-gateway io.springfox springfox-swagger2 com.github.xiaoymin knife4j-spring-ui

10、新建AuthResourceServerConfig.java对gateway网关服务进行配置安全配置,需要使用@EnableWebFluxSecurity而非@EnableWebSecurity,因为SpringCloud Gateway基于WebFlux

package com.gitegg.gateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; import com.gitegg.gateway.auth.AuthorizationManager; import com.gitegg.gateway.filter.WhiteListRemoveJwtFilter; import com.gitegg.gateway.handler.AuthServerAccessDeniedHandler; import com.gitegg.gateway.handler.AuthServerAuthenticationEntryPoint; import com.gitegg.gateway.props.AuthUrlWhiteListProperties; import com.gitegg.platform.base.constant.AuthConstant; import cn.hutool.core.util.ArrayUtil; import lombok.AllArgsConstructor; import reactor.core.publisher.Mono; /** * 资源服务器配置 */ @AllArgsConstructor @Configuration // 注解需要使用@EnableWebFluxSecurity而非@EnableWebSecurity,因为SpringCloud Gateway基于WebFlux @EnableWebFluxSecurity public class AuthResourceServerConfig { private final AuthorizationManager authorizationManager; private final AuthServerAccessDeniedHandler authServerAccessDeniedHandler; private final AuthServerAuthenticationEntryPoint authServerAuthenticationEntryPoint; private final AuthUrlWhiteListProperties authUrlWhiteListProperties; private final WhiteListRemoveJwtFilter whiteListRemoveJwtFilter; @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); // 自定义处理JWT请求头过期或签名错误的结果 http.oauth2ResourceServer().authenticationEntryPoint(authServerAuthenticationEntryPoint); // 对白名单路径,直接移除JWT请求头,不移除的话,后台会校验jwt http.addFilterBefore(whiteListRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); http.authorizeExchange() .pathMatchers(ArrayUtil.toArray(authUrlWhiteListProperties.getUrls(), String.class)).permitAll() .anyExchange().access(authorizationManager) .and() .exceptionHandling() .accessDeniedHandler(authServerAccessDeniedHandler) // 处理未授权 .authenticationEntryPoint(authServerAuthenticationEntryPoint) //处理未认证 .and() .cors() .and().csrf().disable(); return http.build(); } /** * ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication,需要把jwt的Claim中的authorities加入 * 解决方案:重新定义ReactiveAuthenticationManager权限管理器,默认转换器JwtGrantedAuthoritiesConverter */ @Bean public Converter


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有